package hn.sigit.logic.workflow.transaction.impl;

import hn.sigit.dao.hnd.ladmshadow.ParcelDAO;
import hn.sigit.dao.hnd.ladmshadow.PropertyDAO;
import hn.sigit.logic.bpm.AttachedFileData;
import hn.sigit.logic.geometry.BearingAndDistance;
import hn.sigit.logic.geometry.GeometryOperations;
import hn.sigit.logic.geometry.SplitException;
import hn.sigit.logic.geometry.SplitPolygonResult;
import hn.sigit.logic.workflow.transaction.DataEntryHelper;
import hn.sigit.model.hnd.ladmshadow.Parcel;
import hn.sigit.model.hnd.ladmshadow.Property;
import hn.sigit.util.ShapeFile;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;

import org.hibernatespatial.mgeom.MCoordinate;
import org.richfaces.event.UploadEvent;
import org.richfaces.model.UploadItem;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.io.WKTWriter;


public class DataEntrySplitHelper implements Serializable {
	private static final long serialVersionUID = 1L;

	private DataEntryHelper dataEntryHelper;
	
	private List<Coord> currentSplitBorder;
	private List<BearingAndDistance> bearingAndDistances;
	private Coord splitBorderPoint;
	
	//METHOD 2: Variables
	private Coord refPoint;
	private Coord resultingPoint;
	
	private BearingDistance bearingDistance;
	//END METHOD 2 Variables
	
	private AttachedFileData selectedFile;

	
	public DataEntrySplitHelper(DataEntryHelper dataEntryHelper) {
		this.dataEntryHelper = dataEntryHelper;
	}

	public List<Coord> getCurrentSplitBorder() {
		if (currentSplitBorder == null)
			currentSplitBorder = new ArrayList<Coord>();
		return currentSplitBorder;
	}
	public void setCurrentSplitBorder(List<Coord> currentSplitBorder) {
		if (this.currentSplitBorder != currentSplitBorder) {
			this.currentSplitBorder = currentSplitBorder;
			setBearingAndDistances(null);
		}
	}
	
	public List<BearingAndDistance> getBearingAndDistances() {
		if (bearingAndDistances == null) {
			bearingAndDistances = new ArrayList<BearingAndDistance>();

			if (getCurrentSplitBorder().size() > 0) {
				if (getRefPoint().isSet()) {
					Coord c = getCurrentSplitBorder().get(0);
					bearingAndDistances.add(
							BearingAndDistance.newFromCoords(getRefPoint().x, getRefPoint().y, c.x, c.y));
				}
				
				for (int i = 0; i < getCurrentSplitBorder().size() - 1; i++) {
					Coord c1 = getCurrentSplitBorder().get(i);
					Coord c2 = getCurrentSplitBorder().get(i+1);
					bearingAndDistances.add(
							BearingAndDistance.newFromCoords(c1.x, c1.y, c2.x, c2.y));
				}
			}
		}
		return bearingAndDistances;
	}
	public void setBearingAndDistances(List<BearingAndDistance> bearingAndDistances) {
		this.bearingAndDistances = bearingAndDistances;
	}
	
	
	public Coord getSplitBorderPoint() {
		if (splitBorderPoint == null)
			splitBorderPoint = new Coord();
		return splitBorderPoint;
	}
	public void setSplitBorderPoint(Coord splitBorderPoint) {
		this.splitBorderPoint = splitBorderPoint;
	}

	public Coord getRefPoint() {
		if (refPoint == null)
			refPoint = new Coord();
		return refPoint;
	}
	public void setRefPoint(Coord refPoint) {
		this.refPoint = refPoint;
	}

	public Coord getResultingPoint() {
		if (resultingPoint == null)
			resultingPoint = new Coord();
		return resultingPoint;
	}
	public void setResultingPoint(Coord resultingPoint) {
		this.resultingPoint = resultingPoint;
	}

	public BearingDistance getBearingDistance() {
		if (bearingDistance == null)
			bearingDistance = new BearingDistance();
		return bearingDistance;
	}
	public void setBearingDistance(BearingDistance bearingDistance) {
		this.bearingDistance = bearingDistance;
	}

	public Coord getStartingPoint() {
		if (getRefPoint().isSet())
			return getRefPoint();
		else if (getCurrentSplitBorder().size() > 0)
			return getCurrentSplitBorder().get(0);
		
		return new Coord();
	}
	

	public String addPointToBoundary() {
		return addPointToBoundary(null);
	}
	public String addPointToBoundary(Long maxNumPoints) {
		Coord coord = new Coord(splitBorderPoint);
		getCurrentSplitBorder().add(coord);
		
		if (maxNumPoints != null && maxNumPoints > 0) {
			while (getCurrentSplitBorder().size() > maxNumPoints)
				getCurrentSplitBorder().remove(0);
		}

		setBearingAndDistances(null);

		return null;
	}
	public String setBoundaryPoint(int index) {
		Coord coord = new Coord(splitBorderPoint);
		if (index < getCurrentSplitBorder().size())
			getCurrentSplitBorder().set(index, coord);
		else {
			while (getCurrentSplitBorder().size() <= index)
				getCurrentSplitBorder().add(coord);
		}
		
		setBearingAndDistances(null);

		return null;
	}
	public String addBearingDistanceToBoundary() {
		final int splitBorderSize = getCurrentSplitBorder().size();
		
		try {
			if (splitBorderSize > 0) {
				double angle = GeometryOperations.directionToRadAngle(bearingDistance.getBearing());
				double distance = bearingDistance.getDistance();
				
				double dx = distance * Math.cos(angle);
				double dy = distance * Math.sin(angle);
				
				Coord lastCoord = getCurrentSplitBorder().get(splitBorderSize - 1);
				
				Coord newCoord = new Coord(lastCoord.x + dx, lastCoord.y + dy);
				getCurrentSplitBorder().add(newCoord);
				
				setBearingAndDistances(null);
			}
			else {
				FacesContext.getCurrentInstance().addMessage("",
						new FacesMessage(FacesMessage.SEVERITY_ERROR,
								"Error agregando nuevo punto a la frontera",
								"No existe punto de partida para el nuevo rumbo y distancia"));
			}
		}
		catch (IllegalArgumentException e) {
			FacesContext.getCurrentInstance().addMessage("",
					new FacesMessage(FacesMessage.SEVERITY_ERROR,
							"Error de ingreso de datos",
							"El dato de rumbo y distancia ingresado no es correcto"));
		}
		
		return null;
	}
	public String deleteLastBoundaryPoint() {
		List<Coord> coordList = getCurrentSplitBorder();
		if (coordList.size() > 0) {
			coordList.remove(coordList.size() - 1);
			getBearingAndDistances().remove(getBearingAndDistances().size() - 1);
		}
		return null;
	}
	public String clearBoundary() {
		getCurrentSplitBorder().clear();
		getBearingAndDistances().clear();
		setSplitBorderPoint(null);
		setRefPoint(null);
		setResultingPoint(null);
		setBearingDistance(null);

		return null;
	}
	
	public String applyRefOffset() {
		switch (getCurrentSplitBorder().size()) {
		case 0:
			getCurrentSplitBorder().add(new Coord(resultingPoint));
			break;
		case 1:
			Coord coord = getCurrentSplitBorder().get(0);
			coord.setEqualTo(resultingPoint);
			break;
		default:
			Coord firstCoord = getCurrentSplitBorder().get(0);
			double dx = resultingPoint.x - firstCoord.x;
			double dy = resultingPoint.y - firstCoord.y;
			
			for (Coord c : getCurrentSplitBorder()) {
				c.x += dx;
				c.y += dy;
			}
			break;
		}

		return null;
	}
	
	
	
	public String applySplit() {
		//Creates the split border as a linestring object from the input coordinates
		Date timeStamp = new Date();
		
		int borderSize = getCurrentSplitBorder().size();
		
		if (borderSize < 2) {
			FacesContext.getCurrentInstance().addMessage("",
					new FacesMessage(FacesMessage.SEVERITY_ERROR,
							dataEntryHelper.getResBundle().loadMessage("dataentry.split.no_split_possible") + ": ",
							dataEntryHelper.getResBundle().loadMessage("dataentry.split.no_boundary")));
			return null;
		}
		
		Coord c;
		Coordinate[] coordinates = new MCoordinate[borderSize];
		for (int i = 0; i < coordinates.length; i++) {
			c = currentSplitBorder.get(i);
			coordinates[i] = new MCoordinate(c.getX(), c.getY());
		}
		
		Parcel parcelToSplit = (Parcel) dataEntryHelper.getSelectedParcel(); //ParcelDAO.loadParcelById(parcelToSplitId);
		
		if (parcelToSplit == null) {
			FacesContext.getCurrentInstance().addMessage("",
					new FacesMessage(FacesMessage.SEVERITY_ERROR,
							dataEntryHelper.getResBundle().loadMessage("dataentry.split.no_split_possible") + ": ",
							dataEntryHelper.getResBundle().loadMessage("dataentry.split.no_parcel_to_split")));
			return null;
		}

		//proceed to split the parcel geometry object
		Polygon polygonToSplit = parcelToSplit.getShape();
		try {
			SplitPolygonResult spr = GeometryOperations.splitPolygon(polygonToSplit, coordinates);

			//TODO: Version 2: Cambiar a HashMap mapeando de llave a parcela
			List<Parcel> effectiveParcelNeighbors = ParcelDAO.loadNeighborsByParcel(parcelToSplit);
			Map<Long,Parcel> correctedNeighbors = GeometryOperations.eliminateGaps(parcelToSplit, effectiveParcelNeighbors, spr);
			
			/*
			 * NOTE: This commented code is if we want to create a new parcel object version
			 * if we just changed the geometry of this neighbor parcel
			 * 
			for (Parcel oldParcel : effectiveParcelNeighbors) {
				Parcel newParcel = correctedNeighbors.get(oldParcel.getSuID());
				if (newParcel != null) {
					Util.replaceParcel(oldParcel, newParcel, timeStamp, false);
				}
			}
			*/

			/*NOTE: This code only modifies the geometry object without creating
			 * a new version of it
			 */
			for (Parcel neighborParcel : effectiveParcelNeighbors) {
				Parcel correctedParcel = correctedNeighbors.get(neighborParcel.getSuID());
				//since not all neighbors have to be corrected, we have to check for null here
				if (correctedParcel != null) {
					neighborParcel.setShape(correctedParcel.getShape());
					neighborParcel.setModified(true);
					neighborParcel.setOriginal(false);
					ParcelDAO.save(neighborParcel);
				}
			}
			
			
			Property originalProperty = parcelToSplit.getProperty();
			
			
			Property newProperty1 = originalProperty.clone();
			newProperty1.setBeginLifespanVersion(timeStamp);
			
			Parcel newParcel1 = parcelToSplit.clone();
			newParcel1.setBeginLifespanVersion(timeStamp);
			newParcel1.setShape(spr.getSplitPolygon1());
			newParcel1.getBaunits().clear();
			newParcel1.getBaunits().add(newProperty1);
			//TODO: ver lo de la ficha
			newParcel1.setFieldTab(dataEntryHelper.getFieldTabCounter());
			
			newProperty1.getSpatialUnits().clear();
			newProperty1.getSpatialUnits().add(newParcel1);
			
			ParcelDAO.save(newParcel1);
			PropertyDAO.save(newProperty1);

			
			
			Property newProperty2 = originalProperty.clone();
			newProperty2.setBeginLifespanVersion(timeStamp);
			
			Parcel newParcel2 = parcelToSplit.clone();
			newParcel2.setBeginLifespanVersion(timeStamp);
			newParcel2.setShape(spr.getSplitPolygon2());
			newParcel2.getBaunits().clear();
			newParcel2.getBaunits().add(newProperty2);
			//TODO: ver lo de la ficha
			newParcel2.setFieldTab(dataEntryHelper.getFieldTabCounter());
			
			newProperty2.getSpatialUnits().clear();
			newProperty2.getSpatialUnits().add(newParcel2);

			ParcelDAO.save(newParcel2);
			PropertyDAO.save(newProperty2);

			//mark as modified
			parcelToSplit.setModified(true);
			parcelToSplit.setEndLifespanVersion(timeStamp);
			originalProperty.setEndLifespanVersion(timeStamp);
			
			 
			ParcelDAO.save(parcelToSplit);
			PropertyDAO.save(originalProperty);

			this.clearBoundary();
			
			dataEntryHelper.setSelectedParcel(newParcel1);
		}
		catch (SplitException se) {
			FacesContext.getCurrentInstance().addMessage("",
					new FacesMessage(FacesMessage.SEVERITY_ERROR,
							dataEntryHelper.getResBundle().loadMessage(se.getExceptionBundle()) + ": ",
							dataEntryHelper.getResBundle().loadMessage(se.getMessage())));
			se.printStackTrace();
		}
		
		return null;
	}

	public String getSplitBorderWKT() {
		int borderSize = getCurrentSplitBorder().size();
		
		if (borderSize > 1) {
			WKTWriter wktWriter = new WKTWriter();
			Coord c;
			Coordinate[] coordinates = new Coordinate[borderSize];
			for (int i = 0; i < coordinates.length; i++) {
				c = currentSplitBorder.get(i);
				coordinates[i] = new Coordinate(c.getX(), c.getY());
			}

			LineString ls = GeometryOperations.geomFactory.createLineString(coordinates);

			return wktWriter.write(ls);
		}

		return "";
	}
	
	public AttachedFileData getSelectedFile() {
		return selectedFile;
	}
	public void setSelectedFile(AttachedFileData selectedFile) {
		this.selectedFile = selectedFile;
	}

	public void attachFileListener(UploadEvent event) {
		UploadItem item = event.getUploadItem();
		
		try {
			File file = item.getFile();
			String fname = item.getFileName().toUpperCase();
			if (fname.endsWith(".TXT") || fname.endsWith(".SHP")) {
				String ext = fname.substring(fname.lastIndexOf('.'));
				File renamedFile = new File(file.getCanonicalPath() + ext);
				
				if (file.renameTo(renamedFile))
					file = renamedFile;
				else
					throw new IllegalArgumentException("No se pudo abrir el archivo subido al sistema");
				
				AttachedFileData afd = new AttachedFileData();
				afd.setFileName(fname);
				afd.setFile(file);
				
				setSelectedFile(afd);
			}
			else
				throw new IllegalArgumentException("Solamente se aceptan archivos con extension TXT o SHP");
			
			
			file.deleteOnExit();
		}
		catch (Throwable t) {
			FacesContext.getCurrentInstance().addMessage("",
					new FacesMessage(FacesMessage.SEVERITY_ERROR,
							"Error al intentar procesar el archivo " + selectedFile.getFileName() + ": ",
							t.getMessage()));
			t.printStackTrace();
		}
	}

	public void uploadNewFile() {
		setSelectedFile(null);
	}
	public void acceptSelectedFile() {
		if (getSelectedFile() == null) return;
		
		String fname = selectedFile.getFileName().toUpperCase();
		if (fname.endsWith(".TXT"))
			processTextFile();
		else if (fname.endsWith(".SHP"))
			processShapeFile();
		
		setSelectedFile(null);
	}
	
	private void processTextFile() {
		Scanner scanner;
		try {
			scanner = new Scanner(selectedFile.getFile());
			scanner.useDelimiter(",");
			
			String line, northing, easting;
			String[] neLine;
			double x, y;
			int nidx, eidx;
			
			if (scanner.hasNext()) {
				getCurrentSplitBorder().clear();
				do {
					line = scanner.next();
					neLine = line.split(" ", 2);
					
					northing = neLine[0];
					easting = neLine[1];
					
					nidx = northing.indexOf('N');
					eidx = easting.indexOf('E');
					
					try {
						y = nidx != -1 ? Double.parseDouble(northing.substring(0, nidx)) : Double.parseDouble(northing); 
						x = eidx != -1 ? Double.parseDouble(easting.substring(0, eidx)) : Double.parseDouble(northing);

						getCurrentSplitBorder().add(new Coord(x, y));
					}
					catch (NumberFormatException e) {
						e.printStackTrace();
					}
				} while (scanner.hasNext());
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	private void processShapeFile() {
		ShapeFile sf = new ShapeFile(null, selectedFile.getFile());
		if (sf.readShapefile()) {
			getCurrentSplitBorder().clear();
			for (Geometry g : sf.getGeometries()) {
				if (g instanceof LineString) {
					for (Coordinate c : g.getCoordinates()) {
						getCurrentSplitBorder().add(new Coord(c.x, c.y));
					}
					break;
				}
				else if (g instanceof MultiLineString) {
					//we will only consider the first LineString
					MultiLineString mls = (MultiLineString) g;
					if (mls.getNumGeometries() > 0) {
						LineString ls = (LineString) mls.getGeometryN(0);
						for (Coordinate c : ls.getCoordinates()) {
							getCurrentSplitBorder().add(new Coord(c.x, c.y));
						}
					}
				}
				else if (g instanceof Point) {
					Point p = (Point) g;
					getCurrentSplitBorder().add(new Coord(p.getX(), p.getY()));
				}
				else if (g instanceof MultiPoint) {
					//we will consider the first point only
					MultiPoint mp = (MultiPoint) g;
					if (mp.getNumGeometries() > 0) {
						Point p = (Point) mp.getGeometryN(0);
						getCurrentSplitBorder().add(new Coord(p.getX(), p.getY()));
					}
				}
			}
			
			if (getCurrentSplitBorder().size() == 0)
				FacesContext.getCurrentInstance().addMessage("",
						new FacesMessage(FacesMessage.SEVERITY_ERROR,
								"No se encontraron elementos geograficos de punto o linea en el archivo SHP",
								"" ));
		}
		else {
			FacesContext.getCurrentInstance().addMessage("",
					new FacesMessage(FacesMessage.SEVERITY_ERROR,
							"Error al leer el archivo" + ": ",
							"No se pudo leer el archivo " + selectedFile.getFileName()));
		}
	}
	

	public static class Coord {
		private double x;
		private double y;
		private boolean set;
		
		public Coord() {
			this.set = false;
		}
		
		public Coord(double x, double y) {
			this();
			
			setX(x);
			setY(y);
		}
		
		public Coord(Coord other) {
			setX(other.getX());
			setY(other.getY());
		}
		
		public double getX() {
			return x;
		}
		public void setX(double x) {
			this.x = x;
			set = true;
		}
		
		public double getY() {
			return y;
		}
		public void setY(double y) {
			this.y = y;
			set = true;
		}
		
		public void setEqualTo(Coord other) {
			setX(other.getX());
			setY(other.getY());
		}

		public boolean isSet() {
			return set;
		}
		public void setSet(boolean set) {
			this.set = set;
		}
	}
	
	public static class BearingDistance {
		private String bearing;
		private double distance;
		
		public String getBearing() {
			return bearing;
		}
		public void setBearing(String bearing) {
			this.bearing = bearing;
		}
		public double getDistance() {
			return distance;
		}
		public void setDistance(double distance) {
			this.distance = distance;
		}
	}
	
}
